/** (C) Copyright 2011-2014 Chiral Behaviors, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hellblazer.process.impl;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import javax.security.auth.Subject;
import org.apache.commons.io.input.Tailer;
import org.apache.commons.io.input.TailerListener;
import com.hellblazer.process.CannotStopProcessException;
import com.hellblazer.process.JavaProcess;
import com.hellblazer.process.ManagedProcess;
import com.hellblazer.process.NoLocalJmxConnectionException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
/**
* @author Hal Hildebrand
*
*/
public class JavaProcessImpl implements JavaProcess, Cloneable {
private static final long serialVersionUID = 1L;
protected List<String> arguments;
protected File jarFile;
protected String javaClass;
protected File javaExecutable;
protected transient JMXConnector jmxc;
protected ManagedProcess process;
protected List<String> vmOptions;
public JavaProcessImpl(ManagedProcess process) {
assert process != null;
this.process = process;
}
/* (non-Javadoc)
* @see com.hellblazer.process.JavaProcess#addArgument(java.lang.String)
*/
@Override
public void addArgument(String argument) {
if (arguments == null) {
arguments = new ArrayList<String>();
}
arguments.add(argument);
}
/* (non-Javadoc)
* @see com.hellblazer.process.JavaProcess#addArguments(java.lang.String[])
*/
@Override
public void addArguments(String[] args) {
if (arguments == null) {
arguments = new ArrayList<String>();
}
for (String arg : args) {
arguments.add(arg);
}
}
@Override
public void addCommand(String command) {
throw new UnsupportedOperationException(
"Cannot set the command of the process directly");
}
/* (non-Javadoc)
* @see com.hellblazer.process.JavaProcess#addVmOption(java.lang.String)
*/
@Override
public void addVmOption(String vmOption) {
if (vmOptions == null) {
vmOptions = new ArrayList<String>();
}
vmOptions.add(vmOption);
}
/* (non-Javadoc)
* @see com.hellblazer.process.JavaProcess#addVmOptions(java.lang.String[])
*/
@Override
public void addVmOptions(String[] vmOpts) {
if (vmOptions == null) {
vmOptions = new ArrayList<String>();
}
for (String opt : vmOpts) {
vmOptions.add(opt);
}
}
@Override
public JavaProcess clone() {
JavaProcessImpl clone;
try {
clone = (JavaProcessImpl) super.clone();
} catch (CloneNotSupportedException e) {
throw new IllegalStateException("Clone not supported", e);
}
clone.process = process.clone();
if (arguments != null) {
clone.arguments = new ArrayList<String>(arguments);
}
if (vmOptions != null) {
clone.vmOptions = new ArrayList<String>(vmOptions);
}
clone.jarFile = jarFile;
clone.javaClass = javaClass;
return clone;
}
@Override
public JavaProcess configureFrom(ManagedProcess p) {
if (!(p instanceof JavaProcess)) {
throw new UnsupportedOperationException(
"Can only configure from an instance of JavaProcess");
}
JavaProcess javaProcess = (JavaProcess) p;
javaExecutable = javaProcess.getJavaExecutable();
arguments = javaProcess.getArguments();
vmOptions = javaProcess.getVmOptions();
javaClass = javaProcess.getJavaClass();
jarFile = javaProcess.getJarFile();
process.setDirectory(javaProcess.getDirectory());
process.setEnvironment(javaProcess.getEnvironment());
return this;
}
@Override
public void destroy() throws CannotStopProcessException, IOException {
process.destroy();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final JavaProcessImpl other = (JavaProcessImpl) obj;
if (process == null) {
if (other.process != null) {
return false;
}
} else if (!process.equals(other.process)) {
return false;
}
return true;
}
/**
* @return the List of arguments to the Java program
*/
@Override
public List<String> getArguments() {
return new ArrayList<String>(arguments);
}
/**
* @return the List which represents the command to execute this Java
* process
*/
@Override
public List<String> getCommand() {
ArrayList<String> command = new ArrayList<String>();
if (javaExecutable != null) {
command.add(javaExecutable.getAbsolutePath());
}
if (vmOptions != null) {
command.addAll(vmOptions);
}
command.addAll(getExecution());
if (arguments != null) {
command.addAll(arguments);
}
return command;
}
/**
* @return the home directory of the process execution
*/
@Override
public File getDirectory() {
return process.getDirectory();
}
@Override
public Map<String, String> getEnvironment() {
return process.getEnvironment();
}
@Override
public Integer getExitValue() {
return process.getExitValue();
}
/**
* Answer the unique id of this java process
*/
@Override
public UUID getId() {
return process.getId();
}
@Override
public File getJarFile() {
return jarFile;
}
@Override
public String getJavaClass() {
return javaClass;
}
/**
* @return the File which points to the Java executable
*/
@Override
public File getJavaExecutable() {
return javaExecutable;
}
/**
* @throws ConnectException
*/
@Override
public JMXConnector getLocalJmxConnector(String connectorName) throws ConnectException,
NoLocalJmxConnectionException {
if (jmxc != null) {
return jmxc;
}
if (!process.isActive()) {
throw new ConnectException(
"Cannot establish local JMX connection as process is not active: "
+ this);
}
String address;
try {
VirtualMachine vm = VirtualMachine.attach("" + process.getPid());
Properties props = vm.getSystemProperties();
address = props.getProperty(connectorName);
if (address == null) {
throw new ConnectException("Unable to find address for remote JMX connection with name = " + connectorName);
}
} catch (IOException e) {
ConnectException cex = new ConnectException(
"Cannot obtain local JMX connector address of: "
+ this);
cex.initCause(e);
throw cex;
} catch (AttachNotSupportedException e) {
throw new RuntimeException(e);
}
JMXServiceURL jmxUrl;
try {
jmxUrl = new JMXServiceURL(address);
} catch (MalformedURLException e) {
ConnectException cex = new ConnectException(
"Invalid local JMX URL for "
+ this + " : "
+ address);
cex.initCause(e);
throw cex;
}
try {
jmxc = JMXConnectorFactory.connect(jmxUrl);
} catch (java.rmi.ConnectException e) {
if (e.getMessage().startsWith("Connection refused")) {
throw new NoLocalJmxConnectionException(
"Local JMX connector address does not exist for: "
+ this);
}
ConnectException cex = new ConnectException(
"Underlying RMI communications exception");
cex.initCause(e);
throw cex;
} catch (IOException e) {
ConnectException cex = new ConnectException(
"Cannot establish local JMX connection to: "
+ this);
cex.initCause(e);
throw cex;
}
try {
jmxc.connect();
} catch (IOException e) {
ConnectException cex = new ConnectException(
"Cannot establish local JMX connection to: "
+ this);
cex.initCause(e);
throw cex;
}
return jmxc;
}
@Override
public MBeanServerConnection getLocalMBeanServerConnection(String connectionName)
throws ConnectException,
NoLocalJmxConnectionException {
JMXConnector connector = getLocalJmxConnector(connectionName);
try {
return connector.getMBeanServerConnection();
} catch (IOException e) {
ConnectException cex = new ConnectException(
"Cannot establish local JMX connection to: "
+ this);
cex.initCause(e);
throw cex;
}
}
@Override
public MBeanServerConnection getLocalMBeanServerConnection(String connectionName, Subject delegationSubject)
throws ConnectException,
NoLocalJmxConnectionException {
JMXConnector connector = getLocalJmxConnector(connectionName);
try {
return connector.getMBeanServerConnection(delegationSubject);
} catch (IOException e) {
ConnectException cex = new ConnectException(
"Cannot establish local JMX connection to: "
+ this);
cex.initCause(e);
throw cex;
}
}
@Override
public Integer getPid() {
return process.getPid();
}
@Override
public InputStream getStdErr() {
return process.getStdErr();
}
@Override
public String getStdErrTail(int numLines) throws IOException {
return process.getStdErrTail(numLines);
}
@Override
public OutputStream getStdIn() {
return process.getStdIn();
}
@Override
public InputStream getStdOut() {
return process.getStdOut();
}
@Override
public String getStdOutTail(int numLines) throws IOException {
return process.getStdOutTail(numLines);
}
/**
* @return the List of arguments to the Java virtual machine
*/
@Override
public List<String> getVmOptions() {
return new ArrayList<String>(vmOptions);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (process == null ? 0 : process.hashCode());
return result;
}
/**
* @return true if the Java process is active
*/
@Override
public boolean isActive() {
return process.isActive();
}
@Override
public boolean isSameConfiguration(ManagedProcess otherProcess) {
if (!(otherProcess instanceof JavaProcess)) {
return false;
}
JavaProcess other = (JavaProcess) otherProcess;
if (arguments == null) {
if (other.getArguments() != null) {
return false;
}
} else if (!arguments.equals(other.getArguments())) {
return false;
}
if (jarFile == null) {
if (other.getJarFile() != null) {
return false;
}
} else if (!jarFile.equals(other.getJarFile())) {
return false;
}
if (javaClass == null) {
if (other.getJavaClass() != null) {
return false;
}
} else if (!javaClass.equals(other.getJavaClass())) {
return false;
}
if (javaExecutable == null) {
if (other.getJavaExecutable() != null) {
return false;
}
} else if (!javaExecutable.equals(other.getJavaExecutable())) {
return false;
}
if (vmOptions == null) {
if (other.getVmOptions() != null) {
return false;
}
} else if (!vmOptions.equals(other.getVmOptions())) {
return false;
}
return true;
}
/* (non-Javadoc)
* @see com.hellblazer.process.ManagedProcess#restart()
*/
@Override
public void restart() throws IOException {
process.restart();
}
/* (non-Javadoc)
* @see com.hellblazer.process.ManagedProcess#restart(int)
*/
@Override
public void restart(int waitForSeconds) throws IOException {
process.restart(waitForSeconds);
}
@Override
public void setArguments(List<String> arguments) {
if (arguments == null) {
arguments = new ArrayList<String>();
}
this.arguments = new ArrayList<String>(arguments);
}
@Override
public void setArguments(String[] arguments) {
if (arguments == null) {
arguments = new String[] {};
}
ArrayList<String> args = new ArrayList<String>();
for (String argument : arguments) {
args.add(argument);
}
setArguments(args);
}
@Override
public void setCommand(List<String> commands) {
throw new UnsupportedOperationException(
"Cannot set the command of the process directly");
}
@Override
public void setCommand(String[] commands) {
throw new UnsupportedOperationException(
"Cannot set the command of the process directly");
}
@Override
public void setDirectory(File directory) {
process.setDirectory(directory);
}
@Override
public void setDirectory(String directory) {
process.setDirectory(directory);
}
@Override
public void setEnvironment(Map<String, String> environment) {
process.setEnvironment(environment);
}
@Override
public void setJarFile(File jarFile) {
if (jarFile != null) {
javaClass = null; // exclusive or
}
this.jarFile = jarFile;
}
@Override
public void setJarFile(String jarFile) {
if (jarFile == null) {
this.jarFile = null;
return;
}
setJarFile(new File(jarFile));
}
@Override
public void setJavaClass(String javaClass) {
if (javaClass != null) {
jarFile = null; // exclusive or
}
this.javaClass = javaClass;
}
@Override
public void setJavaExecutable(File javaExecutable) {
this.javaExecutable = javaExecutable;
}
@Override
public void setJavaExecutable(String javaExecutable) {
if (javaExecutable == null) {
this.javaExecutable = null;
return;
}
setJavaExecutable(new File(javaExecutable));
}
@Override
public void setVmOptions(List<String> vmOptions) {
if (vmOptions == null) {
vmOptions = new ArrayList<String>();
}
this.vmOptions = new ArrayList<String>(vmOptions);
}
@Override
public void setVmOptions(String[] vmOptions) {
if (vmOptions == null) {
vmOptions = new String[] {};
}
ArrayList<String> options = new ArrayList<String>();
for (String option : vmOptions) {
options.add(option);
}
setVmOptions(options);
}
/**
* Start the Java process
*
* @throws IOException
* - if anything goes awry in starting up the process
*/
@Override
public synchronized void start() throws IOException {
if (javaExecutable == null) {
throw new IllegalStateException("Java executable must not be null");
}
if (vmOptions == null) {
vmOptions = new ArrayList<String>();
}
if (arguments == null) {
arguments = new ArrayList<String>();
}
process.setCommand(getCommand());
process.start();
}
/**
* Stop the execution of the Java process.
*
* @throws CannotStopProcessException
*/
@Override
public synchronized void stop() throws CannotStopProcessException {
process.stop();
}
@Override
public synchronized void stop(int waitForSeconds)
throws CannotStopProcessException {
process.stop(waitForSeconds);
}
/* (non-Javadoc)
* @see com.hellblazer.process.ManagedProcess#tailStdErr(org.apache.commons.io.input.TailerListener)
*/
@Override
public Tailer tailStdErr(TailerListener listener) {
return process.tailStdErr(listener);
}
/* (non-Javadoc)
* @see com.hellblazer.process.ManagedProcess#tailStdErr(org.apache.commons.io.input.TailerListener, long, boolean, boolean, int)
*/
@Override
public Tailer tailStdErr(TailerListener listener, long delayMillis,
boolean end, boolean reOpen, int bufSize) {
return process.tailStdErr(listener, delayMillis, end, reOpen, bufSize);
}
/* (non-Javadoc)
* @see com.hellblazer.process.ManagedProcess#tailStdOut(org.apache.commons.io.input.TailerListener)
*/
@Override
public Tailer tailStdOut(TailerListener listener) {
return process.tailStdOut(listener);
}
/* (non-Javadoc)
* @see com.hellblazer.process.ManagedProcess#tailStdOut(org.apache.commons.io.input.TailerListener, long, boolean, boolean, int)
*/
@Override
public Tailer tailStdOut(TailerListener listener, long delayMillis,
boolean end, boolean reOpen, int bufSize) {
return tailStdOut(listener, delayMillis, end, reOpen, bufSize);
}
@Override
public String toString() {
StringBuffer buf = new StringBuffer();
buf.append("JavaProcess");
buf.append("{").append(getId()).append("} ");
buf.append(" home dir: ");
buf.append(getDirectory());
buf.append(" pid: ");
buf.append(getPid());
return buf.toString();
}
@Override
public int waitFor() throws InterruptedException {
return process.waitFor();
}
/**
* @return the List which represents the arguments to the VM invocation to
* run the Java program
*/
protected List<String> getExecution() {
assert !(jarFile != null && javaClass != null); // can't execute jar and java class simultaneously
ArrayList<String> execution = new ArrayList<String>();
if (jarFile != null) {
execution.add("-jar");
execution.add(jarFile.getAbsolutePath());
} else {
execution.add(javaClass);
}
return execution;
}
}